/*
 * Fire is a fast, themable UI toolkit and xHTML/CSS renderer for mobile application 
 * and game development. It is an eye-candy alternative to the standard midp2 UI 
 * components and unlike them it produces a superior UI result on all mobile devices!
 *  
 * Copyright (C) 2006,2007,2008,2009,2010 Pashalis Padeleris (padeler at users.sourceforge.net)
 * 
 * This file is part of Fire.
 *
 * Fire is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Fire is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 * You should have received a copy of the GNU Lesser General Public License
 * along with Fire.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

/**
 * 
 */
package gr.fire.browser;

import fb.zip.ZipMe;
import fb.zip.ZipUtil;
import gr.fire.browser.util.HttpClient;
import gr.fire.browser.util.Page;
import gr.fire.browser.util.PageListener;
import gr.fire.browser.util.Response;
import gr.fire.core.CommandListener;
import gr.fire.core.Component;
import gr.fire.core.Container;
import gr.fire.core.FireScreen;
import gr.fire.core.Panel;
import gr.fire.core.Theme;
import gr.fire.ui.Alert;
import gr.fire.ui.ProgressbarAnimation;
import gr.fire.ui.TransitionAnimation;
import gr.fire.util.FireConnector;
import gr.fire.util.Lang;
import gr.fire.util.Log;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.InterruptedIOException;
import java.io.UnsupportedEncodingException;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;

import javax.microedition.io.HttpConnection;
import javax.microedition.lcdui.Command;
import javax.microedition.lcdui.Displayable;
import javax.microedition.lcdui.Font;
import javax.microedition.lcdui.Image;

import org.kxml2.io.KXmlParser;

/**
 * The Browser parses XHTML from a given stream and renders it.
 * It has some basic rendering rules that are choosen to improve the 
 * readability and usability of a page when rendered for a small screen. <br/>
 * 
 * * The default width of the browser is the width of the screen.<br/>
 *    
 * * TextPrimitive width is set as the width of the Browser unless noted otherwise 
 *   by the style of the tag.<br/>
 *   
 * * TextPrimitive height is the height of the text calculated using the width 
 *   (according to the rules above) and the font of the text.  <br/>
 *   
 * * ImagePrimitive width is the width of the image unless noted otherwise by the style of the tag. <br/>
 * 
 * @author padeler
 */
public class Browser implements CommandListener, PageListener,ResponseHandler
{
	
	/**
	 * Flag for the imageLoadingPolicy field. 
	 * No images are loaded.
	 */
	public static final byte NO_IMAGES=0x00; 
	/**
	 * Flag for the imageLoadingPolicy field. 
	 * Load images imediatelly
	 */
	public static final byte LOAD_IMAGES=0x01; 
	
	/**
	 * Flag for the imageLoadingPolicy field. This is the default browser behaivior.
	 * Load images in a seperate thread after the full parsing of the page. 
	 * The browser will return the Page object and start a thread to load the rest of the images.
	 * This will apply only for images that the width and height attributes are set.
	 * Images that do not have their width and height attributes set, will be loaded imediatelly.
	 */
	public static final byte LOAD_IMAGES_ASYNC=0x02;

	/**
	 * The listener that will receive the events generated by the page rendered by this browser (i.e. link clicks etc) 
	 * The listener will also receive the submit events generated by forms in the rendered pages. 
	 * If null, then the form handles the submit, with the default way (calls browser.displayPage()) 
	 */
	CommandListener listener = this; 
	PageListener pageListener = this;
	
	/**
	 * If this field is set to false, then the browser will not request the images inside each page
	 */
	byte imageLoadingPolicy = LOAD_IMAGES_ASYNC;  
	
	private final Object lock = new Object();
	private final Object refreshLock = new Object();
	
	/**
	 * The HttpClient used to request resources.
	 */
	HttpClient httpClient;
	
	private Hashtable knownTags = new Hashtable();
	private Hashtable knownContentTypes = new Hashtable();
	private boolean showLoadingGauge=true;	
	
	/* ****** HTML Parsing support variables ******* */  
	private Vector tagStack = new Vector();
	private ProgressbarAnimation gauge=null;
	private Page pageRendering=null;
	private int viewportWidth=-1; 
	private Hashtable htmlHandlerImageCache=null;
	
	private Hashtable htmlCache = new Hashtable();
	private ZipMe zipMe = null;
	/* holds last loaded page image cache. 
											       * These images will be reused in the next page 
											       * or discarded when next page load is completed.
											       */ 
	
	/**
	 * Constructor of a Browser instance with the default HttpClient.
	 * The CommandListener and PageListener are set to this new Browser instance.
	 * @see HttpClient
	 * @see PageListener
	 * @see CommandListener
	 */
	public Browser()
	{
		this(new HttpClient(new FireConnector()));
	}
	
	/**
	 * Constructor of a Browser instance with the supplied httpclient. All other parameters are set to the default
	 * @see #Browser()
	 * @param httpClient
	 */
	public Browser(HttpClient httpClient)
	{
		this.httpClient = httpClient; 
		
		{
			Class ie = new InlineTag().getClass();
			
			registerTag(InlineTag.TAG_A,ie);
			registerTag(InlineTag.TAG_B,ie);
			registerTag(InlineTag.TAG_BR,ie);
			registerTag(InlineTag.TAG_EM,ie);
			registerTag(InlineTag.TAG_I,ie);
			registerTag(InlineTag.TAG_IMG,ie);
			registerTag(InlineTag.TAG_SPAN,ie);
			registerTag(InlineTag.TAG_STRONG,ie);
			registerTag(InlineTag.TAG_BIG,ie);
			registerTag(InlineTag.TAG_SMALL,ie);
			registerTag(InlineTag.TAG_TT,ie);
			registerTag(InlineTag.TAG_U,ie);
			registerTag(InlineTag.TAG_INPUT,ie);
			registerTag(InlineTag.TAG_BUTTON,ie);
			registerTag(InlineTag.TAG_TEXTAREA,ie);
			registerTag(InlineTag.TAG_CENTER,ie);
			registerTag(InlineTag.TAG_LABEL,ie);
			registerTag(InlineTag.TAG_OPTION,ie);
			registerTag(InlineTag.TAG_SELECT,ie);
			registerTag(InlineTag.TAG_TD,ie);
		}		
		
		{
			Class be = new BlockTag().getClass();
			registerTag(BlockTag.TAG_WML,be);		
			registerTag(BlockTag.TAG_CARD,be);		
			
			registerTag(BlockTag.TAG_P,be);		
			registerTag(BlockTag.TAG_HTML,be);		
			registerTag(BlockTag.TAG_BODY,be);		
			registerTag(BlockTag.TAG_TABLE,be);	
			registerTag(BlockTag.TAG_TR,be);

			registerTag(BlockTag.TAG_DIV,be);		
			registerTag(BlockTag.TAG_TITLE,be);		
			registerTag(BlockTag.TAG_META,be);		
			registerTag(BlockTag.TAG_STYLE,be);
			registerTag(BlockTag.TAG_SCRIPT,be);
			registerTag(BlockTag.TAG_H1,be);		
			registerTag(BlockTag.TAG_H2,be);		
			registerTag(BlockTag.TAG_H3,be);		
			registerTag(BlockTag.TAG_H4,be);		
			registerTag(BlockTag.TAG_H5,be);		
			registerTag(BlockTag.TAG_H6,be);
			registerTag(BlockTag.TAG_HR,be);
			registerTag(BlockTag.TAG_FORM,be);
		}		
		
		{
			Class le = new ListBlockTag().getClass();
			registerTag(ListBlockTag.TAG_UL,le);		
			registerTag(ListBlockTag.TAG_LI,le);		
			registerTag(ListBlockTag.TAG_OL,le);		
			registerTag(ListBlockTag.TAG_DL,le);
			registerTag(ListBlockTag.TAG_DT,le);		
			registerTag(ListBlockTag.TAG_DD,le);		
		}
		
		// register unsupported file types
		// The only implemented ResponseHandler is the Browser
		// If you want to implement a custom handler check 
		// the ResponseHandler interface and how it is implemented 
		// in the Browser and the UnsupportedHandler.
		{ 
			UnsupportedHandler uh = new UnsupportedHandler();
			Enumeration ctenum = Response.knownContentTypes.elements();
			while(ctenum.hasMoreElements())
			{
				String ct = (String)ctenum.nextElement();
				if(ct.startsWith("text/html")==false)
				{
					registerContentHandler(ct, uh);
				}
			}
		}
	}
	
	/**
	 * Registers an XML tag to be handled by an instance of the given class. The class MUST be a subclass of Tag
	 * @see HtmlUtil
	 * @param name name of the tag
	 * @param cl the class that will handle the action
	 */
	public void registerTag(String name,Class cl) 
	{
		if(name!=null && cl!=null)
		{
			try
			{
				Tag t = (Tag)cl.newInstance();
			} catch (Exception e)
			{
				Log.logError("Failed to register class for tag "+name+". ",e);
				throw new IllegalArgumentException("Class must be an instantiable subclass of Tag. "+e.getMessage());
			}
			knownTags.put(name,cl);
		}
		else throw new NullPointerException("Tag name and class cannot be null");
	}
	
	
	/**
	 * Registers a new Content handler. The default content handler is the Browser instance which knows how
	 * to handle html documents.
	 * You can override that by registering a different content handler for content-type: text/html
	 * 
	 * @param contentType
	 * @param handler
	 */
	public void registerContentHandler(String contentType,ResponseHandler handler) 
	{
		if(contentType!=null && handler!=null)
		{
			knownContentTypes.put(contentType,handler);
		}
		else throw new NullPointerException("Both ContentType and ResponseHandler cannot be null");
	}

	/**
	 * Loads the page from the given URL using the supplied method and request parameters and data. 
	 * This method will use the supplied HttpClient {@link #httpClient} to make the request and then render the page.<br/> 
	 * 
	 * The resulting will be added to added to a Page instance and returned to the caller.
	 * It will not handle "meta" tag information, but will return them inside the Page instance. <br/>
	 * 
	 * @param url 
	 * @param method The method can be HttpConnection.GET or HttpConnection.POST
	 * @param requestParameters params for the http request header 
	 * @param data if the method is HttpConnection.POST the post data if any must be in this byte array 
	 * @return
	 * @throws UnsupportedEncodingException
	 * @throws IOException
	 * @throws Exception
	 */
	public Page loadPage(String url,String method,Hashtable requestParameters,byte []data) throws UnsupportedEncodingException,IOException,Exception
	{
		synchronized (refreshLock)
		{
			refreshLock.notifyAll();
		}
		
		if(url.startsWith("ttp://") || url.startsWith("ttps://"))
		{
			url = "h"+url;
			Log.logWarn("Malformed url resolved to: "+url);
		}
		
		if (url.equalsIgnoreCase("IndexTop.html"))
		{
			byte[] indexTopBytes = (byte[])htmlCache.get("IndexTop.html");
			if (indexTopBytes != null)
			{
				return loadPage(new ByteArrayInputStream(indexTopBytes), "UTF-8");
			}
		}
		
		try 
		{
            if (url.endsWith(".zip") || url.endsWith(".ZIP")) 
            {
                this.clearHtmlCahce();
                
                zipMe = ZipUtil.unZipFile(url);

                byte[] indexHtmlBytes = zipMe.get("Index.html");

                if (indexHtmlBytes == null) 
                {
                    Log.logError("There is no Index.html in the zip file", new Throwable("no Index.html"));
                    return null;
                }

                htmlCache.put("Index.html", indexHtmlBytes);

                try 
                {
                    return loadPage(new ByteArrayInputStream(indexHtmlBytes), "UTF-8");

                } catch (IOException e) {
                    Log.logError("", e);

                } catch (SecurityException e) {
                    Log.logError("", e);
                }
            }
            
            byte[] htmlBytes = (byte[])htmlCache.get(url);
            if (htmlBytes == null)
            {
                htmlBytes = zipMe.get(url);
            }
            
            if (htmlBytes == null)
            {
                Log.logError("There is no " + url + " in the zip file", null);
            }
            
            this.htmlCache.put(url, htmlBytes);
            
    		if (htmlBytes != null)
    		{
    			return loadPage(new ByteArrayInputStream(htmlBytes), "UTF-8");
    		}
		
		} 
		catch (OutOfMemoryError e) 
        {
            Log.logError("", e);
            this.clearHtmlCahce();
        }
		
		pageListener.pageLoadProgress(httpClient.getAbsolutEncodedURL(url),"Loading",PAGE_LOAD_START,10); // must make a call with PAGE_LOAD_START

		/* ************************** Request the resource **************************** */
		try{
			Response rsp = httpClient.requestResource(url,method,requestParameters,data,true);
			
			pageListener.pageLoadProgress(rsp.getBaseURL(),"Loading",PAGE_LOAD_START,30); // must make a call with PAGE_LOAD_START
			
			return loadPageInternal(rsp);
		}catch(OutOfMemoryError e){
			// disable animations.
			FireScreen.getScreen().setAnimationsEnabled(false);
			// log problem
			Log.logError("Out of memory error, while loading page.",e);
			// inform caller
			throw new Exception("Not enough memory to load page.");
		}
	}
	
	public void clearHtmlCahce()
	{
	    if (htmlCache != null) {
            byte[] indexTopBytes = (byte[]) htmlCache.get("IndexTop.html");
            htmlCache.clear();
            htmlCache.put("IndexTop.html", indexTopBytes);
        }
	}
	
	/**
	 * Loads a page from the given InputStream using the given encoding.
	 * 
	 * @param in
	 * @param encoding
	 * @return A Page instance containing the result of the request
	 * @throws UnsupportedEncodingException
	 * @throws IOException
	 * @throws Exception
	 */
	public Page loadPage(InputStream in,String encoding) throws UnsupportedEncodingException,IOException,Exception
	{
		try{
			return loadPageInternal(new Response(encoding,in));
		}catch(Exception e){
			Log.logError("Failed to request page from stream.",e);
			throw e;
		}
	}
	
	/*
	 * loadPageInternal checks the response content type and gives it to the correct handler for rendering. 
	 * If no suitable handler is found then the default is used. The default handler is the Browser instance. 
	 */
	private Page loadPageInternal(Response rsp) throws UnsupportedEncodingException,IOException,Exception
	{
		/* ********** Notify listener *************** */
		pageListener.pageLoadProgress(rsp.getBaseURL(),Lang.get("Loading"),PAGE_LOAD_LOADING_DATA,40);

		try{
			// select the correct handler for this Content-Type
			String contentType = rsp.getContentType();
			Log.logInfo("Base URL ["+rsp.getBaseURL()+"] File ["+rsp.getFile()+"] Content-Type ["+rsp.getContentType()+"]");
			ResponseHandler h = (ResponseHandler)knownContentTypes.get(contentType);
			if(h!=null) return h.handleResponse(rsp, this);
			// else Default ResponseHandler is always this Browser (which is an HTML ResponseHandler)
			return handleResponse(rsp, this);
		}catch(Exception e){
			Log.logError("Failed to request page "+rsp.getURL()+".",e);
			throw e;
		}finally{
			pageListener.pageLoadProgress(rsp.getURL(),Lang.get("Loading"),PAGE_LOAD_END,100);
			if(!rsp.isClosed())
			{
				Log.logWarn("ResponseHandler did not close the Response streams. Closing.");
				try{
					rsp.close();
				}catch(IOException ex){
					Log.logWarn("Response Connection not closed!", ex);					
				}
			}
		}
	}
	
	/*
	 * The main loop of the Browser module. This uses the XmlPullParser to parse the xml from the inputstream and 
	 * then iterates through the tags of the document. Each known tag is handled by the class that is registered to handle it
	 * using the registerTag method.
	 */
	/**
	 * The Browser implements the default ResponseHandler which is for Content-Type: text/html
	 */
	public Page handleResponse(Response response,Browser browser) throws UnsupportedEncodingException,IOException,Exception
	{
		Page page = new Page(response.getURL());
		synchronized (lock)
		{
			if(pageRendering!=null)
			{
				pageRendering.setCanceled(true);
			}
			pageRendering=page;
		}
		/* Create the Page object that will be returned. */
		/* ******************************** clean old page stuff here **************************** */
		tagStack.removeAllElements();
		
		pageListener.pageLoadProgress(page.getUrl(),Lang.get("Loading"),PAGE_LOAD_LOADING_DATA,45);
		
		if(response.isCanceled())
		{
			page.setCanceled(true);
			return page; 
		}
		
		InputStreamReader reader=null;
		try{
			String encoding = response.getEncoding();
			Log.logDebug("Using Encoding: "+encoding);
			int contentLength=-1;
			String lengthStr=response.getHeaderField("Content-Length");
			if(lengthStr!=null)
			{
				try{ 
					contentLength = Integer.parseInt(lengthStr.trim()); 
				} catch(NumberFormatException ex){
					Log.logWarn("Failed to parse content-length of html page: "+lengthStr,ex);
				}
			}
			InputStream in = response.getInputStream();
			
			ByteArrayOutputStream bout = readFully(in,contentLength); // preload the full document
			
			reader = new InputStreamReader(new ByteArrayInputStream(bout.toByteArray()), encoding);
			bout.close();
			in=null;
			bout=null;
			
			try{ // close the response. It is no longer needed to remain open.
				response.close();
			}catch(Throwable e) {
				Log.logWarn("Failed to close response",e);
			}
			pageListener.pageLoadProgress(page.getUrl(),Lang.get("Loading"),PAGE_LOAD_PARSING_DATA,50);
			
			KXmlParser parser = new KXmlParser();
			parser.setInput(reader);
			parser.setFeature(org.xmlpull.v1.XmlPullParser.FEATURE_RELAXED,true);
			int type=-1,oldType=-1;
		
			Theme th = FireScreen.getTheme();
			Tag rootTag = new InlineTag();
			rootTag.setForegroundColor(th.getIntProperty("xhtml.fg.color"));
			rootTag.setBackgroundColor(th.getIntProperty("xhtml.bg.color"));
			rootTag.setFont(th.getFontProperty("xhtml.font"));
			
			/* ********** Main XML parsing loop **************** */
			String name=null;
			try{
				while (!page.isCanceled())
				{
					if(type==oldType) type= parser.next(); // only progress parser if a tag didnt already call parser.next()
					oldType = type; // some tags call parser.next(), old type helps keep track if parser.next() was called.
					
					if(type==KXmlParser.START_TAG) /* **** Handle Opening TAGs ***** */
					{
						name = parser.getName().toLowerCase();;
						Class tagClass = (Class)knownTags.get(name);
						if(tagClass!=null)
						{
							Tag t = (Tag)tagClass.newInstance();
							
							if(tagStack.size()==0)
								t.inheritStyle(rootTag); // inherit basic style information.
							
							t.handleTagStart(this,page,parser);
							pushTag(t);
						}
						else
						{
						    if (!name.equalsIgnoreCase(BlockTag.TAG_WML) && !name.equalsIgnoreCase(BlockTag.TAG_CARD))
						    {
//						        Log.logWarn("Unknown Opening TAG "+name);
						    }
						}
					}
					else if(type==KXmlParser.END_TAG) /* **** Handle Closing TAGs ***** */
					{
						name = parser.getName().toLowerCase();;
						Tag t = (Tag)topTag();
						if(t!=null && name.equals(t.getName()))
						{
							t.handleTagEnd(this,page,parser);
							popTag();
							if(tagStack.size()==0 &&( t.getName().equals(BlockTag.TAG_HTML) ||  t.getName().equals(BlockTag.TAG_WML))) // END OF DOCUMENT.
							{
								break;
							}
						}
						else
						{
						    if (!name.equalsIgnoreCase(BlockTag.TAG_WML) && !name.equalsIgnoreCase(BlockTag.TAG_CARD))
						    {
//						        Log.logWarn("Unknown Closing TAG "+name+" expected "+(t==null?"none":t.getName()));
						    }
						}
					}
					else if(type==KXmlParser.TEXT) /* **** Handle Text inside a TAG ***** */
					{
						Tag top = (Tag)topTag();
						
						String txt = parser.getText();
						if(top!=null && txt.length()>0)
						{
							top.handleText(top,txt);
						}
					}
					else if(type==KXmlParser.END_DOCUMENT)
					{
						Log.logDebug("=>End Of Document<=");
						break; // parsing completed.
					}
					else /* **** Default action, just log the unknown type and continue **** */
					{ 
					    if (!name.equalsIgnoreCase(BlockTag.TAG_WML) && !name.equalsIgnoreCase(BlockTag.TAG_CARD))
					    {
					        Log.logWarn("Unknown tag "+parser.getName() +" type " + type);
					    }
					}
					type  = parser.getEventType(); // get type again since some tags call parser.next()
				}
			}catch(InterruptedIOException e){
				// the execution was canceled by a Browser.cancel() call.
				Log.logInfo("XHTML Rendering Canceled by USER.");
				page.setCanceled(true);
			}catch(InstantiationException e)
			{
				Log.logError("Failed to instantiate a Tag class for tag name "+name+".",e);
			} catch (Exception e)
			{
				Log.logError("Exception while handling tag start "+name,e);
			}
			pageListener.pageLoadProgress(page.getUrl(),Lang.get("Loading"),PAGE_LOAD_PARSING_DATA,100);				
		}finally{
			try{
				if(reader!=null) reader.close();
			}catch(Throwable e) {}
			
			synchronized (lock)
			{
				pageRendering=null;
			}
		}
		// replace the old image cache with the new one
		htmlHandlerImageCache = page.getImageCache();
		
		if(imageLoadingPolicy==LOAD_IMAGES_ASYNC && page.isCanceled()==false && page.getAsyncImageLoadList()!=null && page.getPageContainer()!=null)
		{ // start async loading only if rendering was not stoped using Browser.stop()
			page.startAsyncImageLoad(this);
		}

		return page;
	}
	
	private ByteArrayOutputStream readFully(InputStream in,int len) throws IOException
	{
		
		byte buf[] = new byte[2048];
		int s;
		ByteArrayOutputStream bout;
		if(len>0) bout = new ByteArrayOutputStream(len);
		else bout = new ByteArrayOutputStream(buf.length);
		int count=0;
		
		while((s=in.read(buf))>-1 && (len==-1 || count<len))
		{
			bout.write(buf,0,s);
			count+=s;
		}
		Log.logInfo("HTML document loaded "+count+" bytes. (content-length: "+len+")");
		if(len>-1)
		{
			if(count>len)
			{
				Log.logWarn("Got more bytes that content-length indicated:");
				Log.logWarn("["+new String(bout.toByteArray(),len,count-len)+"]");
			}
		}
		return bout;
	}
	
	/**
	 * When a Tag is pushed it is considered to be inside the last one pushed. 
	 * The Tag implementation is responsible for doing so.
	 *  
	 * @param node
	 */
	private void pushTag(Tag node)
	{
		tagStack.addElement(node);
	}
	
	public Tag topTag()
	{
		if(tagStack.size()>0)
			return (Tag)tagStack.lastElement();
		// else
		return null;
	}

	
	private Tag popTag()
	{
		int size = tagStack.size();
		if(size>0)
		{
			Tag tc = (Tag)tagStack.lastElement();
			tagStack.removeElementAt(size-1);
			
			if(size<=3)
				pageListener.pageLoadProgress(null,Lang.get("Rendering"),PAGE_LOAD_PARSING_DATA,-1); // easy (but not so aquarate) method to show progress relative the parsing of the page... 
			
			return tc;
		}
		return null;
	}
		
	/**
	 * The Browser instance is the default CommandListener for any rendered page. Use setListener method to use your
	 * own custom listener.
	 * @see #setListener(CommandListener)
	 * @see gr.fire.core.CommandListener#commandAction(javax.microedition.lcdui.Command, gr.fire.core.Component)
	 */
	public void commandAction(Command command, Component c)
	{
		if(command instanceof gr.fire.browser.util.Command)
		{ // only handle known command types
			gr.fire.browser.util.Command cmd = (gr.fire.browser.util.Command)command;
			String url = cmd.getUrl();
			loadPageAsync(url,HttpConnection.GET,null,null);
		}
	}
	/**
	 * The Browser instance is the default PageListener for loadPageAsync requests. Use the setPageListener method to
	 * use your own custom pageListener
	 * @see PageListener
	 * @see #setPageListener(PageListener)
	 */
	public void pageLoadCompleted(String url,String method,Hashtable requestParams, Page page)
	{		
		// hide gauge
		if(showLoadingGauge) FireScreen.getScreen().removeComponent(6);
		if(page!=null)
		{
			if(page.getUserMessage()!=null)
			{
				FireScreen.getScreen().showAlert(page.getUserMessage(),page.getUserMessageType(), Alert.USER_SELECTED_OK, null,null);
			}
			Container cnt = page.getPageContainer();
			String title= page.getPageTitle();
			Log.logInfo("Loaded Page ["+url+"]["+title+"]");

			if(cnt!=null && page.isCanceled()==false)
			{
				Panel panel = new Panel(cnt,Panel.VERTICAL_SCROLLBAR|Panel.HORIZONTAL_SCROLLBAR,true);
				panel.setLabel(title);
				
				Component current = FireScreen.getScreen().getCurrent();
				Command left=null,right=null;
				if(current!=null)
				{
					left = current.getLeftSoftKeyCommand();
					right = current.getRightSoftKeyCommand();
				}
				
				panel.setLeftSoftKeyCommand(left);
				panel.setRightSoftKeyCommand(right);
				FireScreen screen = FireScreen.getScreen();
				Component last = screen.getCurrent();
				
				if(last!=null) // show a transition animation
				{
					panel.setAnimation(new TransitionAnimation(panel,TransitionAnimation.TRANSITION_SCROLL|TransitionAnimation.TRANSITION_RIGHT));
				}
				
				FireScreen.getScreen().setCurrent(panel);
				panel.setCommandListener(listener);
				panel.setDragScroll(true);
				return;
			}
			else // if cnt is null then the action was canceled by the user (using Browser.cancel()).
			{
				return;
			}
		}
		// Error case. Alert user
		String t=url;
		if(url.length()>15) t = url.substring(0,15);  
		FireScreen.getScreen().showAlert(Lang.get("Failed to load page")+": "+t,Alert.TYPE_ERROR,Alert.USER_SELECTED_OK,null,null);
	}
	
	/**
	 * This is the asynchronous version of the loadPage{@link #loadPage(String, String, Hashtable, byte[])} method.
	 * It will start a new thread to handle the request and it will send the result Page to the registered PageListener 
	 * instead of returning it to the caller.
	 * 
	 * @see #loadPage(InputStream, String)
	 * 
	 * @param url
	 * @param method
	 * @param requestParameters
	 * @param data
	 */
	public void loadPageAsync(final String url,final String method,final Hashtable requestParameters,final byte []data)
	{
		Thread th = new Thread()
		{
			public void run()
			{
				try
				{
					Page pageMeta = loadPage(url,method,requestParameters,data);
	
					if(pageMeta!=null)
					{
						if(pageMeta.isCanceled())
						{
							pageListener.pageLoadFailed(url,method,requestParameters,new InterruptedIOException("User cancel"));
							return;
						}

						pageListener.pageLoadCompleted(url,method,requestParameters,pageMeta);
						
						if(pageMeta.getRefresh()!=null)
						{
							long milis =  1000* pageMeta.getRefreshSeconds();
							if(milis>0)
							{
								long before= System.currentTimeMillis();
								synchronized (refreshLock)
								{
									try{
										refreshLock.wait(milis);}catch(InterruptedException e){
										Log.logDebug("LoadPageAsync thread interrupted while waiting on the refreshLock for "+milis+"ms.");
									}
								}
								if(System.currentTimeMillis()-before<milis) // waiting was interrupted by Browser.cancel() or Browser.loadPage().
								{
									Log.logWarn("Meta-Refresh canceled.");
									return;
								}
							}
							Component current = FireScreen.getScreen().getCurrent();
							if((current instanceof Panel && ((Panel)current).getComponent(0)==pageMeta.getPageContainer()) ||
								current==pageMeta.getPageContainer()) // only execute refresh, if the user is still on the same page
							{
								loadPageAsync(pageMeta.getRefresh(),HttpConnection.GET,null,null);
							}
							else 
							{
								Log.logWarn("Ignoring refresh to "+pageMeta.getRefresh() );
							}
						}

					}
					else{
						Log.logWarn("loadPageAsync: loadPage returned null page");
						pageListener.pageLoadFailed(url, method, requestParameters, new NullPointerException("No page loaded."));
					}
				}catch(OutOfMemoryError e){ // when out of memory occurs, Fire must disable animation. 
					FireScreen.getScreen().setAnimationsEnabled(false);
					throw e;
				}catch (Throwable er)
				{
					Log.logWarn("Async load failed",er);
					pageListener.pageLoadFailed(url,method,requestParameters,er);
				}
			}
		};
		th.start();
	}
	
	public Image loadImage(InputStream is)
	{
		try {
			return Image.createImage(is);
		} catch (IOException e) {
			Log.logError("failed to load image", e);
			return null;
		}
	}
	
	/**
	 * Utility method to easily load an image using the httpclient of the browser instance.
	 * If an out of memory occures during loadImage, the method 
	 * will disable the FireScreen animations.
	 * @param url
	 * @return the image on the given url.
	 * @throws InterruptedIOException 
	 */
	public Image loadImage(String url) throws InterruptedIOException
	{
		Response r=null;
		try{
			r = httpClient.requestResource(url,HttpConnection.GET,null,null,false);
			if(r!=null && (r.getResponseCode()==HttpConnection.HTTP_OK))
			{
				Image img=null;
				String lengthStr =null;
				if((lengthStr=r.getHeaderField("Content-Length"))!=null) // try to load based on length
				{
					int len = Integer.parseInt(lengthStr.trim());
					if(len<=0 || (len*2)>Runtime.getRuntime().freeMemory())
						throw new Exception("Not enought memory to load image.");
					
					byte []buf = new byte[len];
					InputStream in = r.getInputStream();
					int s=0,total=0;
					while(s!=-1 && total<len) // load data to the buffer
					{
						s = in.read(buf, total, len-total);
						if(s>0) total += s;
					}
					// create image from buffer 
					if(total>0) img = Image.createImage(buf,0,total);
				}
				else
				{
					img = Image.createImage(r.getInputStream());					
				}
				return img;
			}
		}catch(OutOfMemoryError e){
			System.gc();
			FireScreen.getScreen().setAnimationsEnabled(false);
			Log.logWarn("Out-of-Memory on load image from "+url,e);
		}catch(Throwable e){
			if(e instanceof InterruptedIOException)
			{
				throw (InterruptedIOException)e; // throw the exception to notify caller for user cancel. 
			}
			Log.logWarn("Failed to load image from: "+url,e);
		}finally{
			try{
				if(r!=null) r.close();
			}catch(Exception e){
				Log.logWarn("Failed to close request in Browser.loadImage.",e);
			}
		}
		return null;
	}
	
	
	public void commandAction(Command cmd, Displayable d)
	{
	}

	/**
	 * The Browser renders each page based on a set width and unbounded height. The viewportWidth is 
	 * the width of a rendered page. If the page contains elements that do not fit in the viewpoerWidth
	 * It will increase the width of the resulting page ignoring the viewportWidth.
	 *  
	 * @return
	 */
	public int getViewportWidth()
	{
		if(viewportWidth<=0) return FireScreen.getScreen().getWidth();
		
		return viewportWidth;
	}
	

	/**
	 * Sets the width of the screen that the browser will use to render properly each Page.
	 * @param viewportWidth
	 */
	public void setViewportWidth(int viewportWidth)
	{
		this.viewportWidth = viewportWidth;
	}

	/**
	 * Returns the CommandListener that is set to handle the Browser requests
	 * @return
	 */
	public CommandListener getListener()
	{
		return listener;
	}

	/**
	 * Overides the default listener for link and form events for all rendered pages of this Browser intance.
	 * The default listener is the Browser instance itself.
	 * 
	 * @param listener The CommandListener for link and form events. If null then the Browser instance if used (default)
	 */
	public void setListener(CommandListener listener)
	{
		if(listener==null) listener=this;
		this.listener = listener;
	}

	/**
	 * Returns the HttpClient instance that this Browser instance will use to make Http Requests to http servers. 
	 * @return
	 */
	public HttpClient getHttpClient()
	{
		return httpClient;
	}

	/**
	 * Sets the HttpClient of this Browser instance.
	 * @see #getHttpClient()
	 * @param httpClient
	 */
	public void setHttpClient(HttpClient httpClient)
	{
		this.httpClient = httpClient;
	}

	/**
	 * There are different policies on loading the images of a Page.<br/> 
	 * - Browser.NO_IMAGES <br/>
	 * - Browser.LOAD_IMAGES<br/>
	 * - Browser.LOAD_IMAGES_ASYNC  (default) <br/>
	 * 
	 * The load LOAD_IMAGES_ASYNC will skip images with preset width and height (as img tag properties) and will
	 * try to load them after the Page is done loading. This greatly speeds up Page loading. 
	 * 
	 * @return
	 */
	public byte getImageLoadingPolicy()
	{
		return imageLoadingPolicy;
	}

	/**
	 * @see #getImageLoadingPolicy()
	 * @param imageLoadingPolicy
	 */
	public void setImageLoadingPolicy(byte imageLoadingPolicy)
	{
		this.imageLoadingPolicy = imageLoadingPolicy;
	}

	/**
	 * Returns the PageListener for loadPageAsync requests. The default pagelistener is the Browser.
	 * @see  #pageLoadCompleted(String, String, Hashtable, Page)
	 * @see #pageLoadFailed(String, String, Hashtable, Throwable)
	 * @return
	 */
	public PageListener getPageListener()
	{
		return pageListener;
	}

	/**
	 * @see #getPageListener()
	 * @param pageListener
	 */
	public void setPageListener(PageListener pageListener)
	{
		if(pageListener==null) pageListener=this;
		this.pageListener = pageListener;
	}

	public void pageLoadFailed(String url, String method, Hashtable requestParams, Throwable error)
	{
		if(error instanceof OutOfMemoryError)
		{
			System.gc();
			try
			{
				// try to disable animations
				FireScreen.getScreen().setAnimationsEnabled(false);
				Log.logError("Out of Memory! Request to URL ["+url+"] failed",error);
				FireScreen.getScreen().showAlert(Lang.get("Could not load page. Out of memory!"),Alert.TYPE_ERROR,Alert.USER_SELECTED_OK,null,null);
			}catch (OutOfMemoryError e)
			{
				System.gc();
			}			
		}
		else if(error instanceof InterruptedIOException)
		{ // user canceled the execution.
			Log.logInfo("Loading of ["+url+"] canceled by user.");
		}
		else 
		{
			Log.logError("Request to URL ["+url+"] failed",error);
			FireScreen.getScreen().showAlert(Lang.get("Error loading page. ")+" "+error.getMessage(),Alert.TYPE_ERROR,Alert.USER_SELECTED_OK,null,null);			
		}
		// hide gauge
		if(showLoadingGauge) FireScreen.getScreen().removeComponent(6);
	}

	public boolean isShowLoadingGauge()
	{
		return showLoadingGauge;
	}

	public void setShowLoadingGauge(boolean showLoadingGauge)
	{
		this.showLoadingGauge = showLoadingGauge;
	}
	
	/**
	 * If there is a rendering process on this Browser instance this method will cancel it.
	 * This method will not close any connections or interrupt i/o operations. 
	 * It will only prevent the data from beeing rendered.
	 * 
	 * If images are beeing loaded asynchronously a call to this method will also stop
	 * the image loading thread.
	 * 
	 */
	public void cancel()
	{
		if(pageListener!=null)
		{
			pageListener.pageLoadFailed(httpClient.getCurrentURL(),HttpConnection.GET,null,new InterruptedIOException("Canceled by user"));
		}		
		try{
			httpClient.cancel();
		}catch(Exception e){
			Log.logWarn("Failed to close rendering response on Browser.cancel",e);
		}
		
		stopRendering();
		
		synchronized (refreshLock)
		{// wake up all threads waiting for meta-refresh timeout. (see loadPageAsync())
			refreshLock.notifyAll();
		}
	}
	
	
	public void stopRendering()
	{
		synchronized (lock)
		{
			if(pageRendering!=null)
			{
				pageRendering.setCanceled(true);
			}
		}		
	}

	/**
	 * The default implamentation of this method.
	 * @see PageListener#pageLoadProgress(String, byte, int)
	 */
	public void pageLoadProgress(String url, String message, byte state, int percent)
	{
		if(!showLoadingGauge) return;
		
		switch(state)
		{
		case PAGE_LOAD_START:
			if(gauge!=null){
				FireScreen.getScreen().removeComponent(gauge);
				gauge=null;
			}
			gauge = new ProgressbarAnimation(message);
			
			Font font = FireScreen.getTheme().getFontProperty("titlebar.font");
			FireScreen screen = FireScreen.getScreen();
			int sw = screen.getWidth();
			//int mw = font.stringWidth(message);
			gauge.setWidth(sw);
			gauge.setHeight(font.getHeight());
			gauge.setPosition(0,0); // top left corner of the fire screen.
			screen.addComponent(gauge,6);
			break;
		case PAGE_LOAD_END:
			if(gauge!=null){
				FireScreen.getScreen().removeComponent(6);
				gauge=null;
			}
		case PAGE_LOAD_LOADING_DATA:
			if(gauge!=null)
			{
				gauge.setMessage(Lang.get("Loading"));
				if(percent>0) gauge.progress(percent);
				else gauge.progress();
			}
			break;
		case PAGE_LOAD_PARSING_DATA:
			if(gauge!=null)
			{
				gauge.setMessage(Lang.get("Rendering"));
				if(percent>0) gauge.progress(percent);
				else gauge.progress();
			}
			break;
		}
	}
	
	public Image getCachedImage(String id)
	{
		return (htmlHandlerImageCache!=null)?(Image)htmlHandlerImageCache.get(id):null;
	}

	public Hashtable getHtmlCache() {
		return this.htmlCache;
	}

    public ZipMe getZipMe() {
        return zipMe;
    }
}